Skip to content

Closing an account, with 30 days to change your mind - #17

Merged
jagdish-kumawat merged 3 commits into
mainfrom
account-closure
Sep 21, 2026
Merged

jagdish-kumawat merged 3 commits into
mainfrom
account-closure

Conversation

@jagdish-kumawat

Copy link
Copy Markdown
Contributor

Builds the one thing the terms and privacy policy promised that nothing did: an owner can close the account, everything is kept for 30 days so it can be brought back, and then it is deleted. Closes the last "Not started" line of the master plan's Phase 1. ADR 0045 records the decision.

Three commits, each leaving both editions building and every suite green.

The engine (2315fc5)

  • Closing is closing the organisation. Measurement stops for every website at once, nobody in it can open its screens, and everything it holds is kept whole for Organization.ClosureRetention (30 days). Any owner can bring it back; after that a sweep deletes its telemetry, its rows, and the people who belonged nowhere else. The last organisation's purge empties the install, and the first-run claim stays shut through an installation_claims marker (AddOrganizationClosure migration, back-filled for every install that already has accounts).
  • OrganizationDirectory.StandingForAsync prefers an open organisation, so somebody who also belongs to an open account is never walled out; IAccountClosure.DescribeForAsync says whether they have anywhere open to be.
  • A closed organisation is absent from every read and write path — the site list, the collector (beacon and server key refused at once), the roster, invitation links — except the session, the two closure endpoints (POST/DELETE /api/organization/closure, 409 AccountAlreadyClosed/AccountNotClosed), an edition's observers and the sweep.
  • IAccountClosureObserver is the seam an edition uses for money: called inside the closing and restoring transactions, and last at the purge, so the one irreversible thing is the last thing attempted.
  • Three messages to every owner — closed, a week before deletion, restored — with idempotency keys that fold in the recipient, which also fixes the existing owner broadcasts that reached only the first owner.
  • The hourly sweep claims each reminder under the account's advisory lock, and handles each account in a scope of its own.

The dashboard (d63c838)

  • Settings → Close this account (owners only), confirmed by typing the account's name.
  • A closed-account screen for anybody whose only account is closed, wherever they go: who closed it, when it is deleted, "Bring it back" for an owner, "Sign out" for everyone. Password-reset and invitation links keep working while closed; joining an open account frees a walled person.
  • Somebody who also belongs to an open account keeps their dashboard and sees one line in the bar ("Name is closed · Bring it back").
  • destinationFor() decides from session.closure and hasOpenAccount; closing and restoring write the session from the answer and mark everything else read about the account stale; a 409 because somebody else acted first settles the cache the same way. EditionModule.closure is the slot an edition fills with what it has to say about money.
  • ClosureCopyTests holds the "30 days" on the card to the engine's retention.

Docs (fa571c3)

  • Master plan line → Done; the self-host guide says what closing does on your own server.

Verified locally

Both editions build with zero warnings. Community suites green (Domain 120, Application 289, Architecture 26, Api 14, MailTests 24, Integration incl. AccountClosureTests and ClosedAccountPurgeTests on real containers). pnpm verify green (1,273 dashboard tests). 44 Playwright states of the new screens looked at in light and dark at phone and desktop width; the three emails rendered and read.

The private repository's matching change (closure: ClosurePlan and the hosted service's billing side) builds against main once this is merged; the required EditionModule.closure member means the two land back to back. Production runs the Cloud edition, so this is released together with that change.

🤖 Generated with Claude Code

jagdish-kumawat and others added 3 commits September 21, 2026 18:10
Closing is closing the organisation: measurement stops for every website at
once, nobody in it can open its screens, and everything it holds is kept whole
for Organization.ClosureRetention so that an owner can bring it back. After
that a sweep deletes its telemetry, its rows, and the people who belonged
nowhere else; the last organisation's purge empties the install and the
first-run claim stays shut through an installation_claims marker.

Where somebody stands prefers an open organisation, so a person who also
belongs to an open account is not walled out. A closed organisation is absent
from every read and write path except the session, the two closure endpoints,
an edition's observers and the sweep. Every owner is written to on closure,
a week before deletion, and on restore, with keys that fold in the recipient.

ADR 0045 records the decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…a closed one

An owner closes the account from the Account screen by typing its name. Anybody
whose only account is closed lands on one screen wherever they go: it names the
account, who closed it and when, when it will be deleted, and offers an owner
"Bring it back". Somebody who also belongs to an open account keeps their
dashboard and sees one line in the bar about the closed one. Password-reset
and invitation links keep working while closed, and joining an open account
frees a walled person.

The gate decides from session.closure and hasOpenAccount in one pure function.
Closing and restoring write the session from the answer and mark everything
else read about the account stale; a refusal because somebody else acted a
moment earlier settles the cache the same way and re-reads the session. The
edition seam gains a closure slot for what an edition has to say about money.
ClosureCopyTests holds the "30 days" on the card to the engine's retention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… says what closing does

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jagdish-kumawat
jagdish-kumawat merged commit 849b272 into main Sep 21, 2026
7 checks passed
@jagdish-kumawat
jagdish-kumawat deleted the account-closure branch September 21, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant